iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0

簡介

在鐵人賽的開篇,我們介紹了應用程式的開發過程,涵蓋了 Web Application 和 Command Line Tool 的開發。隨後,我們討論了如何透過工具提升程式碼的可讀性與維護性。

接下來的幾個章節將聚焦於另一個提高應用程式品質的關鍵要素——測試。當開發者完成新功能後,通常會啟動應用程式,然後透過瀏覽器或終端進行測試,這種方式被稱為手動測試(Manual Testing)。然而,手動測試有一個顯著的缺點:每次執行測試都需要投入一定的時間和精力。因此,開發者通常只會針對本次新增的功能進行測試。隨著應用程式規模的擴大,新功能或更改可能會影響到現有功能的正常運行,而手動測試無法全面確保新更改不會破壞舊有的功能。

相對於手動測試,自動化測試則是另一個有效的方法。由於自動化測試能夠快速重複執行大量測試,開發者可以利用它來驗證舊有功能,確保新更改不會破壞現有功能,從而有效解決之前提到的問題。雖然建立自動化測試在初期需要投入時間和資源,但從長期來看,這將顯著提高測試效率並確保軟體產品的整體品質。

之前針對應用程式所撰寫的稱為 Product Code,這些 Product Code 共同構成了一個完整的軟體產品。而自動化測試的構建同樣需要涉及程式碼,開發者必須撰寫額外的程式碼,這些程式碼被稱為 Test Code。Product Code 與 Test Code 就如同行走時的雙腳,缺一不可,缺少任何一隻腳都會導致行走不穩。Product Code 的目標是確保業務邏輯符合產品需求,而 Test Code 則用來檢測產品代碼是否滿足這些需求。

Test Code 的結構通常劃分為三個區域,俗稱 3A。接下來,我將結合範例程式碼分別介紹這三個 A。

  1. Arrange
    在這一步,開發者需要準備測試環境和所需資料,這通常包括初始化物件、設置變數等。安排的目的是為測試提供必要的上下文,確保其能正確運行。
    在範例中,開發者準備了變數 ab,其值分別為 5 與 6。

  2. Act
    此步驟涉及執行測試的核心動作,即呼叫需要測試的函式。
    在範例中,開發者呼叫了想要測試的 sum 函式,並取得其返回值。

  3. Assert
    最後,開發者需要檢查測試結果是否符合預期。這通常涉及使用 Assert 來驗證函式的返回值或系統狀態是否與預期一致。
    在範例中,開發者驗證返回值是否等於預期的 11。如果符合預期,則代表 sum 這個函式有符合軟體需求。

def sum(a: int, b: int) -> int:
    return a + b

def test_sum():
    # Arrange
    a = 5
    b = 6

    # Act
    result = sum(a, b)

    # Assert
    assert result == 11

在大多數測試情境中,Arrange 通常是最繁瑣的環節,因此許多 Python 測試套件都特別注重 Arrange 的部分,旨在幫助開發者更輕鬆地撰寫這一階段的代碼。

本篇文章將介紹的測試套件是 Pytest。Pytest 針對 Arrange 步驟提供了一個名為 Fixture 的特性,使開發者能夠輕鬆設置和共享測試前的 Arrange 步驟。接下來,我將通過範例來介紹 Pytest Fixture。

範例

本次範例使用的是 Pytest 8.3.3 版本

poetry add pytest==8.3.3

以購物平台的商品為例,首先,我們需要建立一個 product 目錄,然後在該目錄內創建 model.py 文件。在這個文件中,可以直接使用之前 Pydantic 章節中介紹的 Product 類別。

from pydantic import BaseModel, PositiveInt


class Product(BaseModel):
    name: str
    price: PositiveInt

接著,建立一個 handler.py 文件,其中包含兩個函式。第一個函式根據傳入的金額參數,篩選出所有價格高於該金額的商品名稱。第二個函式則負責找出價格最貴的商品名稱。

from .model import Product


def get_expensive_product_names(
    products: list[Product], price_threshold: int
) -> list[str]:
    return [product.name for product in products if product.price > price_threshold]


def get_most_expensive_product_name(products: list[Product]) -> str:
    return max(products, key=lambda product: product.price).name

最後,我們回到專案目錄下的 tests 目錄,該目錄在使用 poetry new 指令時已經自動生成。接著,我們在 tests 目錄下建立 test_product.py 文件。這段 Test Code 可以分為三個區塊。

首先,第一個區塊使用了 Pytest 的 Fixture,我們可以在 Fixture 中進行 Arrange,即宣告測試所需的變數。通常,Fixture 的函式名稱會代表 Arrange 的變數名稱。比如,這次我們宣告了一組產品,因此 Fixture 可以命名為 products

接下來,第二個區塊是測試 expensive_product_names 函式。Pytest 預設會自動執行所有以 test 開頭的測試案例,因此測試案例的名稱通常是 test_ 加上想要測試的函式名稱。
此外,當測試案例的參數名稱與 Fixture 名稱相同時,Pytest 會在執行測試時自動執行對應的 Fixture,並將結果傳入測試案例中。以第一個測試情境為例,因為測試案例的參數名稱是 products,Pytest 會先執行 products Fixture,成功初始化一系列商品,然後將這些商品傳入測試案例中。因此,在這個情境下,我們不需要重新宣告一組產品變數。

同理,第二個測試情境也是如此,直接使用 Fixture 來獲得一組商品。這樣的設計讓我們只需建置一次 Fixture,將通用性高的變數封裝在其中,就可以在多個測試情境中重複使用,這不僅節省了開發成本,還提升了測試情境的可讀性。

import pytest
from product.handler import get_expensive_product_names, get_most_expensive_product_name
from product.model import Product


# Arrange
@pytest.fixture
def products() -> list[Product]:
    return [
        Product(name="Apple", price=30),
        Product(name="Orange", price=25),
        Product(name="Phone", price=30000),
        Product(name="TV", price=48888),
        Product(name="Toilet Paper", price=299),
        Product(name="Earphone", price=6000),
    ]

def test_get_expensive_product_names(products):
    # Arrange
    price_threshold = 100

    # Act
    product_names = get_expensive_product_names(products, price_threshold)

    # Assert
    assert product_names == ["Phone", "TV", "Toilet Paper", "Earphone"]


def test_get_most_expensive_product_name(products):
    # Act
    product_name = get_most_expensive_product_name(products)

    # Assert
    assert product_name == "TV"

我們只需要執行 poetry run pytest 即可看到測試的結果。
https://ithelp.ithome.com.tw/upload/images/20241007/20168663MaBV5Pei3f.png


上一篇
[Day 23] Mypy
下一篇
[Day 25] Freezegun
系列文
Python 不止於數據,開發應用程式它也在行!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言